feat(loop): decouple driver lifetime from daemon — transient systemd unit per driver, re-attach, halt helper (#119) - #122
Merged
Conversation
…unit per driver (#119) Spawns each PR-loop driver as a transient `systemd --user` unit (pr-loop-driver-issue<N>/pr-loop-driver-pr<N>) via `systemd-run --user --wait --collect` with RuntimeMaxSec as the wall-clock ceiling, so a daemon restart or crash (KillMode=control-group) no longer kills an in-flight driver — only the daemon's disposable systemd-run waiter dies. Falls back to the existing setsid+timeout spawn when systemd-run isn't on PATH. Adds startup re-attach (wait for an already-active driver unit instead of ticking, then run the existing post-exit verify + ledger path), a census guard against advancing an issue whose driver unit is active, and loop-halt.sh for stopping one/all drivers or everything by hand. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…est coverage (#119) Post-review fixes for the driver-lifetime-decouple change: - systemd-run --wait relays rc=143 for a RuntimeMaxSec kill (not GNU timeout's 124/137); run_driver now normalizes it via the unit's own Result property so timeout classification/verify-skip converge with the fallback path. - run_driver now detects a systemd-run spawn/connect failure (via a start-marker the wrapped command touches first) and falls through to the setsid+timeout fallback instead of silently ledgering a phantom spawn-error. - reset-failed guard before every systemd-run spawn to avoid colliding with a stale failed unit under the same deterministic name. - loop-halt.test.sh scenario 2 and new loop-census.test.sh scenarios use a curated PATH (excluding systemctl) so the no-systemd degrade paths are deterministically exercised regardless of host. - loop-daemon.test.sh gains scenarios 14/15 covering the rc-normalization and spawn-failure-fallback paths. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
robercano
approved these changes
Jul 15, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #119.
What & why
Driver lifetime was coupled to the daemon: a daemon restart/crash tore down the whole cgroup (driver + orchestrator + agents), and
setsid/timeout 90mdied with it — an orphaned driver had no ceiling. This decouples the two.Changes
loop-daemon.shrun_driver): spawns viasystemd-run --user --wait --collect --unit=pr-loop-driver-<issueN|prN>withRuntimeMaxSecfromLOOP_DRIVER_TIMEOUTas the hard ceiling (enforced by the user manager, which outlives the daemon).--waitis a disposable waiter that propagates the exit code; the existing ledger + issue Loop: driver one-shot contract needs enforcement + tests — background orchestration produced phantom-success drivers and wedged the queue #111 post-exit verify logic is reused unchanged.systemd-runspawn/connect failure (e.g. no--userbus on the legacy-cron path); on failurerun_driverfalls through to the originalsetsid timeoutspawn, running the driver exactly once.systemctl --user reset-failedguards against stale same-named failed units.RuntimeMaxSeckill relays 143, so after--waitthe unit'sResult=timeoutis read back and rc normalized to 124 — systemd and fallback paths converge on identical ledger/verify handling.main()): before the first tick, waits for any still-activepr-loop-driver-*unit instead of double-spawning, then runs Loop: driver one-shot contract needs enforcement + tests — background orchestration produced phantom-success drivers and wedged the queue #111 post-exit verification on its outcome.loop-census.sh): never advances an issue whose driver unit is active. No-op without systemd.loop-halt.sh(new ops helper): stop one driver (issueN/prN/unit), all drivers (--drivers), or everything (--all, daemon + drivers). Degrades cleanly without systemd.docs/USAGE.md): daemon + failure-contract sections flipped from "restart kills in-flight drivers" to "restart is driver-safe"; documentsloop-halt.shand thesystemctl --user stopcommands.RuntimeMaxSecpassthrough, exit-code propagation, systemd-path timeout normalization, spawn-failure fallback, startup re-attach, census guard (active + no-systemd), andloop-halt.shargument/degrade paths.Gates
build / lint / test all green (
GATES_FILE=.claude/self/gates.json). Suites: loop-daemon 74/74, loop-halt 18/18, loop-census 11/11, plus self-adapter checks + fan-out smoke.Review
Correctness (opus) + tests (sonnet) lenses both APPROVE after one reject to fix cycle (rc-normalization, reachability fallback, and test host-independence were the fixes).
Known limitation
The
systemctl show -p Resulttimeout read-back races--collectGC; if GC wins, a killed driver ledgersresult=exit rc=143unnormalized (rare, same class as the existingwait_for_driver_unitrace — documented, not a blocker).Complementary to #111 (this eliminates the restart-collateral kill class; #111 heals what a host reboot/sleep/
wsl --shutdownstill kills).🤖 Generated with Claude Code